/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package terminalapp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author Kostya
 */
public class ConnectionHandler {

    class EscSequence{

        public EscSequence(int[] aStart, int aEnd) {
            startSeq = aStart;
            endSeq = aEnd;
        }

        public EscSequence(int[] aStart, int aEnd, int aFixedLength) {
            startSeq = aStart;
            endSeq = aEnd;
            fixedParamLength = aFixedLength;
        }

        int[] startSeq = {};
        int endSeq = 0;
        boolean mark = false;
        int fixedParamLength = -2;
    }

    List<EscSequence> sequences = new ArrayList<EscSequence>();
    boolean inSequence = false;
    int sequencePos = 0;
    List<Integer> sequenceBody = new ArrayList<Integer>();
    boolean startChecked = false;

    private InputStream inputStream = null;
    private OutputStream outputStream = null;
//    private static String TERM = "XTERM-COLOR";
    private static String TERM = "ANSI";
    private static String SPEED = "9600,9600";
    private TerminalModel model = new TerminalModel(this);
    private int[] charBytes = new int[4];
    private int charBytesPos = 0;

    private byte[] buildArray(Object... aParams){
        List<Integer> arr = new ArrayList<Integer>();
        for (Object object : aParams) {
            if(object instanceof Number){
                Number number = (Number) object;
                arr.add((int)number.byteValue());
            }else{
                String s = object.toString();
                char[] chars = s.toCharArray();
                for (int i = 0; i < chars.length; i++) {
                    char c = chars[i];
                    arr.add((int)c);
                }
            }
        }
        byte[] bytes = new byte[arr.size()];
        for (int i = 0; i < arr.size(); i++) {
            bytes[i] = arr.get(i).byteValue();

        }
        return bytes;
    }

    private byte[] processCommand(List<Integer> aCommand){
        if(aCommand.size()==0){
            return null;
        }
        Log.debug(this, "processCommand", aCommand.get(0));
        switch (aCommand.get(0)){
            case 24://TERMINAL-TYPE
                return buildArray(255, 250, 24, 0, TERM, 255, 240);
            case 32://SPEED
                return buildArray(255, 250, 32, 0, SPEED, 255, 240);
            case 39://NEW-ENVIR
//                for (Integer i : aCommand) {
//                    Log.debug(this, "command 39 "+i);
//                }
                return buildArray(255, 250, 39, 0,
//                        0, "COLUMNS", 1, Integer.toString(model.lineWidth),
//                        0, "LINES", 1, Integer.toString(model.winHeight),
                        255, 240);
            case 33:
                for (Integer i : aCommand) {
                    Log.debug(this, "command 33 "+i);
                }
                return buildArray(255, 250, 33, 0, 255, 240);
        }
        return null;
    }

    void sendChar(int aByte) throws IOException{
        if(outputStream!=null)
            outputStream.write(aByte);
            outputStream.flush();
    }

    private byte[] acceptCommand(int aCommandType, int aCommandValue){
        Log.debug(this, "Question about", aCommandType, aCommandValue);
        int resp = 0;
        switch (aCommandType){
            case 251://Will
                resp = 253;
                break;
            case 253://Do
                resp = 251;
                if(aCommandValue==35)
                    resp = 252;
                if(aCommandValue==33)
                    resp = 252;
                break;
            case 252://Will not
                resp = 254;
                break;
            case 254://Do not
                resp = 252;
                break;
        }
        return new byte[]{(byte)255, (byte)resp, (byte)aCommandValue};
    }

    private int getUTFCharacter(int[] bytes) {
        int i = -1;
        int j = bytes[0];
        if (j == -1) {
            return -2;
        }
        j &= 0xff;
        boolean flag = false;
        switch (j >> 4) {
            case 8: // '\b'
            case 9: // '\t'
            case 10: // '\n'
            case 11: // '\013'
            case 0: // '\0'
            case 1: // '\001'
            case 2: // '\002'
            case 3: // '\003'
            case 4: // '\004'
            case 5: // '\005'
            case 6: // '\006'
            case 7: // '\007'
            default:
                i = j;
                break;

            case 12: // '\f'
            case 13: // '\r'
                i = j & 0x1f;
                i <<= 6;
                int k = bytes[1];
                if(k==-1)
                    return -1;
                if ((k & 0xc0) != 128) {
                    return -2;
                }
                i += k & 0x3f;
                break;

            case 14: // '\016'
                i = j & 0xf;
                i <<= 6;
                int l = bytes[1];
                if(l==-1)
                    return -1;
                if ((l & 0xc0) != 128) {
                    return -2;
                }
                i += l & 0x3f;
                i <<= 6;
                int l2 = bytes[2];
                if(l2==-1)
                    return -1;
                if ((l2 & 0xc0) != 128) {
                    return -2;
                }
                i += l2 & 0x3f;
                break;
        }
        return i;
    }

    private void clearCharBytes(){
        for (int i = 0; i < charBytes.length; i++) {
            charBytes[i] = -1;
        }
        charBytesPos = 0;
    }

    private void plainCharReceived(int aChar){
//        System.out.print((char)aChar);
//        System.out.flush();
//        Log.debug(this, "Char", Integer.toHexString(aChar), (char)aChar);
        charBytes[charBytesPos] = aChar;
        int multi = getUTFCharacter(charBytes);
        if(multi==-2){//Error - skip char
            Log.error(this, "ERROR reading utf!");
            clearCharBytes();
            return;
        }
        if(multi!=-1){
//            Log.debug(this, "("+model.x+","+model.y+")Write char:", multi, (char)multi);
            model.acceptCharacter(multi);
            clearCharBytes();
        }else
            charBytesPos++;
    }

    public ConnectionHandler() {
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x41));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x42));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x43));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x44));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x45));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x46));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x47));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x48));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x4a));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x4b));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x50)); //P
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x53));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x54));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x66));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x68));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x6c));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x6d));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x70));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x72)); //r
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x73));
        sequences.add(new EscSequence(new int[]{27, 0x5b}, 0x75));
        sequences.add(new EscSequence(new int[]{27, 0x5d}, 0x7));
        sequences.add(new EscSequence(new int[]{27, 0x28}, 0x28, 1));
        sequences.add(new EscSequence(new int[]{27, 0x29}, 0x29, 1));
        sequences.add(new EscSequence(new int[]{27, 0x2a}, 0x2a, 1));
        sequences.add(new EscSequence(new int[]{27, 0x2b}, 0x2b, 1));
        sequences.add(new EscSequence(new int[]{27, 0x37}, -1)); //ESC 7 Save Cursor (DECSC)
        sequences.add(new EscSequence(new int[]{27, 0x38}, -1)); //ESC 8 Restore Cursor (DECRC)
        sequences.add(new EscSequence(new int[]{27, 0x3d}, -1)); //ESC = Restore Cursor (DECRC)

    }
    
    public void start(String aHost, int aPort) {
        try {
            Socket socket = new Socket();
            socket.connect(new InetSocketAddress(aHost, aPort));
            inputStream = socket.getInputStream();
            outputStream = socket.getOutputStream();
            int b = -1;
            boolean command = false;
            boolean commandCollect = false;
            int commandType = 0;
            int commandValue = 0;
            List<Integer> intParams = new ArrayList<Integer>();
            List<Integer> param = new ArrayList<Integer>();
            List<List<Integer>> escParams = new ArrayList<List<Integer>>();
            boolean escMode = false;
            int escModeConfirmed = 0;
            int escType = 0;
            while((b = inputStream.read())!=-1){
//                Log.debug(this, "Received byte:", b, Integer.toHexString(b), (char)b);
//                Log.debug(this, "("+model.x+","+model.y+")Receive char:", Integer.toHexString(b), (char)b);
                switch(b){
                    case 255:
                        command = true;
                        break;
                    case 251:
                    case 252:
                    case 253:
                    case 254:
                    case 250:
                        if(command){
                            commandType = b;
                            commandCollect = commandType==250;
                            intParams.clear();
//                            Log.debug(this, "Receive command character", commandCollect);
                            break;
                        }
                    case 240:
                        if(command && commandCollect){
                            command = false;
                            byte[] bytes = processCommand(intParams);
                            if(bytes!=null){
                                outputStream.write(bytes);
                                outputStream.flush();
                            }
                        }
                        break;
                    default:
                        if(command){
                            if(commandCollect){
                                intParams.add(b);
                            }else{
                                commandValue = b;
                                byte[] response = acceptCommand(commandType, commandValue);
                                if(response!=null)
                                    outputStream.write(response);
                                command = false;
                                if(commandValue==31){//NAWS
                                    outputStream.write(buildArray(255, 250, 31, 0, model.lineWidth, 0, model.winHeight, 255, 240));
                                }
                            }
                        }else{
                            if(!inSequence){
                                //First, look for matched sequence
                                startChecked = false;
                                for (EscSequence seq : sequences) {
                                    if(seq.startSeq[0]==b){//First matched
//                                        Log.debug(this, "Start collecting sequence", b, seq.endSeq, (char) seq.endSeq);
                                        inSequence = true;
                                        sequencePos = 0;
                                        seq.mark = true;
                                        if(seq.startSeq.length==1)//Only one start symbol
                                            startChecked = true;
                                    }else
                                        seq.mark = false;
                                }
                                if(!inSequence)
                                    plainCharReceived(b);
                                else{
//                                    Log.debug(this, "Start sequence mode from ", b);
                                }
                            }else{
                                if(!startChecked){
                                    //We dont have matched sequence - check next symbol
//                                    Log.debug(this, "Next symbol is", (char)b, b);
                                    sequencePos++;
                                    for (EscSequence seq : sequences) {
                                        if(!seq.mark)//Not matched
                                            continue;
                                        if(seq.startSeq[sequencePos]==b){
                                            //Continue check
                                            if(seq.startSeq.length-1==sequencePos){
                                                //End of start sequence
                                                startChecked = true;
                                                if(seq.endSeq==-1){
                                                    //Report also
                                                    model.acceptEscSequence(seq, sequenceBody);
                                                    inSequence = false;
                                                }
                                            }
                                        }else{
                                            seq.mark = false;
                                        }
                                    }
                                }else{
                                    //We have matched sequence
                                    for (EscSequence seq : sequences) {
                                        if(!seq.mark)//Not matched
                                            continue;
                                        if(seq.endSeq==b ||
                                                seq.fixedParamLength-1==sequenceBody.size()){
                                            if(seq.fixedParamLength-1==sequenceBody.size()){
                                                //Copy to body
                                                sequenceBody.add(b);
                                            }
                                            //We found sequence, report
//                                            Log.debug(this, "Report sequence ", (char)seq.endSeq);
                                            model.acceptEscSequence(seq, sequenceBody);
                                            inSequence = false;
                                            sequenceBody.clear();
                                            break;
                                        }
                                    }
                                    if(inSequence){
//                                        Log.debug(this, "Copy as param", b, (char)b);
                                        sequenceBody.add(b);//Still in sequence
                                    }
                                }
                            }
                            
//                            if(escMode && escModeConfirmed!=0){
//                                param.add(b);
//                            }else{
//                                escMode = false;
//                                plainCharReceived(b);
//                            }
                        }
                }
            }
            inputStream = null;
            outputStream = null;
            socket.close();
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        Log.debug(this, "Connection is closed");
    }

    public TerminalModel getModel() {
        return model;
    }

}
